package ipmanager.common;

public class IP implements Comparable {

	private String addressIP;

	private String decIP;

	private String hexaIP;

	private String binIP;

	/**
	 * Constructor de la clase IP
	 * 
	 * @param addressIP
	 * @param ipFormat
	 *            BINARY, DECIMAL, HEXA
	 * @throws Exception
	 */
	public IP(String addressIP, String ipFormat) throws Exception {

		// Valida que la direccion IP no tenga caracteres invalidos
		boolean validChars = isValidChars(addressIP, ipFormat);
		if (!validChars)
			throw new Exception("Direccion IP " + addressIP
					+ " tiene caracteres invalidos");

		// Valida que la direccion IP tenga los cuatros grupos (separados por .)
		boolean validNumberOfGroups = isValidNumberOfGroups(addressIP);
		if (!validNumberOfGroups)
			throw new Exception("Direccion IP " + addressIP
					+ " no tiene los cuatro grupos requeridos");

		this.addressIP = addressIP;

		// Si ipFormat es null o decimal
		if ((ipFormat == null) || (ipFormat.equals(IPFormat.DECIMAL))) {

			this.decIP = addressIP;
			this.binIP = this.decToBinIP(this.decIP);
			this.hexaIP = this.binToHexa(this.binIP);
		}

		// Si ipFormat es binario
		else if (ipFormat.equals(IPFormat.BINARY)) {

			this.binIP = addressIP;
			this.decIP = this.binToDecIP(this.binIP);
			this.hexaIP = this.binToHexa(this.binIP);

		}

		// Si ipFormat es hexa
		else if (ipFormat.equals(IPFormat.HEXA)) {

			this.hexaIP = addressIP;
			this.binIP = this.hexaToBinIP(this.hexaIP);
			this.decIP = this.binToDecIP(this.binIP);
		}

		// Valida que la direccion IP no se encuentre dentro del grupo de
		// direcciones invalidas
		boolean specialIP = isSpecialIP(this.decIP);
		if (specialIP)
			throw new Exception("Direccion IP " + addressIP
					+ " se encuentra dentro de las direcciones especiales");

		// Chequeo si la IP es valida
		if (!this.isDecIPValid(this.decIP))
			throw new Exception("Direccion IP " + this.decIP + " invalida");
	}

	/**
	 * Valida que la direccion IP no se encuentre dentro del grupo direcciones
	 * especiales
	 * 
	 * @param addressIP
	 * @return specialIP
	 */
	private boolean isSpecialIP(String decIP) {

		boolean specialIP = false;

		// Si es igual a 127.0.0.0 (loopback)
		if (decIP.equals("127.0.0.0"))
			specialIP = true;

		// Si es todos 1 o todos 0
		if (decIP.equals("255.255.255.255") || decIP.equals("0.0.0.0"))
			specialIP = true;

		// Otras direcciones especiales
		// Clase A: 10.0.0.0 a 10.255.255.255 (8 bits red, 24 bits hosts)
		// Clase B: 172.16.0.0 a 172.31.255.255 (12 bits red, 20 bits hosts)
		// Clase C: 192.168.0.0 a 192.168.255.255 (16 bits red, 16 bits hosts)

		return specialIP;
	}

	/**
	 * Valida que una direccion IP tenga los cuatros grupos requeridos
	 * 
	 * @param addressIP
	 * @return
	 */
	private boolean isValidNumberOfGroups(String addressIP) {

		// Divido la direccion IP en grupos, separados por "."
		String groups[] = addressIP.split("\\.");
		// Es valida, si la cantidad de grupos es 4
		return (groups.length == 4);
	}

	/**
	 * Chequea si la direccion IP tiene caracteres validos
	 * 
	 * @param addressIP2
	 * @param ipFormat
	 * @return
	 */
	private boolean isValidChars(String addressIP, String ipFormat) {

		// Obtengo los caracteres validos dependiendo del formato
		String availableChars = null;
		if (ipFormat.equals(IPFormat.BINARY)) {
			availableChars = ".01";
		} else if (ipFormat.equals(IPFormat.DECIMAL)) {
			availableChars = ".0123456789";
		} else if (ipFormat.equals(IPFormat.HEXA)) {
			availableChars = ".0123456789ABCDEF";
		}

		// Chequeo si la direccion IP tiene algun caracter que no esta dentro de
		// los permitidos
		boolean validChars = true;
		for (int i = 0; i < addressIP.length(); i++) {
			if (availableChars.toUpperCase().indexOf(
					addressIP.toUpperCase().charAt(i)) == -1) {
				validChars = false;
				break;
			}
		}

		return validChars;
	}

	/**
	 * Valida si una direccion IP (decimal) es valida
	 * 
	 * @param addressIP
	 * @return dec IP valid
	 */
	private boolean isDecIPValid(String addressIP) {
		String[] parts = addressIP.split("\\.");
		for (String s : parts) {
			int i = Integer.parseInt(s);
			if (i < 0 || i > 255)
				return false;
		}
		return true;
	}

	/**
	 * Convierte una direccion IP decimal a binario
	 * 
	 * @param decIP
	 * @return binIP
	 */
	private String decToBinIP(String decIP) {
		String[] parts = decIP.split("\\.");
		String[] binParts = new String[4];
		int i = 0;
		for (String part : parts) {

			// Convertir la parte a binario
			int decPart = Integer.parseInt(part);
			String binPart = Integer.toBinaryString(decPart);

			// Si la parte tiene menos de 8 digitos, agregar 0's
			if (binPart.length() < 8) {
				StringBuffer zeros = new StringBuffer();
				for (int k = 0; k < 8 - binPart.length(); k++)
					zeros.append("0");
				binPart = zeros.toString() + binPart;
			}

			binParts[i] = binPart;
			i++;
		}
		return binParts[0] + "." + binParts[1] + "." + binParts[2] + "."
				+ binParts[3];
	}

	/**
	 * Convierte una direccion IP binaria a hexa
	 * 
	 * @param binIP
	 * @return hexaIP
	 */
	private String binToHexa(String binIP) {
		String[] parts = binIP.split("\\.");
		String[] hexaParts = new String[4];
		int i = 0;
		for (String part : parts) {

			// Convertir la parte a hexa
			int binPart = Integer.parseInt(part, 2);
			String hexaPart = (Integer.toHexString(binPart)).toUpperCase();

			// Si la parte tiene menos de 2 digitos, agregar un 0
			if (hexaPart.length() < 2) {
				hexaPart = "0" + hexaPart;
			}

			hexaParts[i] = hexaPart;
			i++;
		}
		return hexaParts[0] + "." + hexaParts[1] + "." + hexaParts[2] + "."
				+ hexaParts[3];
	}

	/**
	 * Convierte una direccion IP binaria a decimal
	 * 
	 * @param binIP
	 * @return decIP
	 */
	private String binToDecIP(String binIP) {
		String[] parts = binIP.split("\\.");
		String[] decParts = new String[4];
		int i = 0;
		for (String part : parts) {

			// Convertir la parte a dec
			int dec = Integer.parseInt(part, 2);
			String decPart = String.valueOf(dec);

			decParts[i] = decPart;
			i++;
		}
		return decParts[0] + "." + decParts[1] + "." + decParts[2] + "."
				+ decParts[3];
	}

	/**
	 * Convierte una direccion IP hexa a binaria
	 * 
	 * @param hexaIP
	 * @return binIP
	 */
	private String hexaToBinIP(String hexaIP) {
		String[] parts = hexaIP.split("\\.");
		String[] binParts = new String[4];
		String[] hex = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A",
				"B", "C", "D", "E", "F" };
		String[] binary = { "0000", "0001", "0010", "0011", "0100", "0101",
				"0110", "0111", "1000", "1001", "1010", "1011", "1100", "1101",
				"1110", "1111" };

		int i = 0;
		for (String part : parts) {

			// Convertir la parte a bin
			String binPart = "";
			for (int k = 0; k < part.length(); k++) {
				char temp = part.charAt(k);
				String temp2 = "" + temp + "";
				for (int j = 0; j < hex.length; j++) {
					if (temp2.equalsIgnoreCase(hex[j])) {
						binPart = binPart + binary[j];
					}
				}
			}

			binParts[i] = binPart;
			i++;
		}
		return binParts[0] + "." + binParts[1] + "." + binParts[2] + "."
				+ binParts[3];
	}

	/**
	 * Retorna direccion IP binaria
	 * 
	 * @return binIP
	 */
	public String getBinIP() {
		return binIP;
	}

	/**
	 * Retorna direccion IP decimal
	 * 
	 * @return decIP
	 */
	public String getDecIP() {
		return decIP;
	}

	/**
	 * Retorna direccion IP hexa
	 * 
	 * @return hexaIP
	 */
	public String getHexaIP() {
		return hexaIP;
	}

	/**
	 * Retorna direccion IP (original)
	 * 
	 * @return addressIP
	 */
	public String getAddressIP() {
		return addressIP;
	}

	/**
	 * Retorna la direccion IP como resultado de la operacion AND entre la de
	 * esta clase y la pasada como parametro
	 * 
	 * @param anotherIP
	 * @return andIP
	 * @throws Exception
	 */
	public IP andOperation(IP anotherIP) throws Exception {
		String[] binIP1 = this.getBinIP().split("\\.");
		String[] binIP2 = anotherIP.getBinIP().split("\\.");

		StringBuffer andParts = new StringBuffer();

		// Para cada una de las 4 partes de una direccion IP
		for (int i = 0; i < 4; i++) {

			// Tomo la parte i
			String binPart1 = binIP1[i];
			String binPart2 = binIP2[i];
			String[] andPart = new String[8];

			// Recorro cada uno de los bits de la parte i
			for (int k = 0; k < 8; k++) {

				if ((binPart1.charAt(k) == '1') && (binPart2.charAt(k) == '1')) {
					andPart[k] = "1";
				} else
					andPart[k] = "0";
			}

			// Agrego el resultado de la parte actual
			for (int k = 0; k < 8; k++) {
				andParts.append(andPart[k]);
			}

			if (i < 3)
				andParts.append(".");
		}

		IP andOp = new IP(andParts.toString(), IPFormat.BINARY);

		return andOp;
	}

	/**
	 * Retorna la direccion IP como resultado de la operacion OR entre la de
	 * esta clase y la pasada como parametro
	 * 
	 * @param anotherIP
	 * @return andIP
	 * @throws Exception
	 */
	public IP orOperation(IP anotherIP) throws Exception {
		String[] binIP1 = this.getBinIP().split("\\.");
		String[] binIP2 = anotherIP.getBinIP().split("\\.");

		StringBuffer andParts = new StringBuffer();

		// Para cada una de las 4 partes de una direccion IP
		for (int i = 0; i < 4; i++) {

			// Tomo la parte i
			String binPart1 = binIP1[i];
			String binPart2 = binIP2[i];
			String[] andPart = new String[8];

			// Recorro cada uno de los bits de la parte i
			for (int k = 0; k < 8; k++) {

				if ((binPart1.charAt(k) == '1') || (binPart2.charAt(k) == '1')) {
					andPart[k] = "1";
				} else
					andPart[k] = "0";
			}

			// Agrego el resultado de la parte actual
			for (int k = 0; k < 8; k++) {
				andParts.append(andPart[k]);
			}

			if (i < 3)
				andParts.append(".");
		}

		IP andOp = new IP(andParts.toString(), IPFormat.BINARY);

		return andOp;
	}

	/**
	 * Retorna la IP negada
	 * 
	 * @return negativeIP
	 * @throws Exception
	 */
	public IP noneOperation() throws Exception {

		String[] binIP = this.getBinIP().split("\\.");

		StringBuffer noneParts = new StringBuffer();

		// Para cada una de las 4 partes de una direccion IP
		for (int i = 0; i < 4; i++) {

			// Tomo la parte i
			String binPart = binIP[i];
			String[] nonePart = new String[8];

			// Recorro cada uno de los bits de la parte i
			for (int k = 0; k < 8; k++) {
				if (binPart.charAt(k) == '1')
					nonePart[k] = "0";
				else if (binPart.charAt(k) == '0')
					nonePart[k] = "1";
			}

			// Agrego el resultado de la parte actual
			for (int k = 0; k < 8; k++) {
				noneParts.append(nonePart[k]);
			}

			if (i < 3)
				noneParts.append(".");
		}

		IP noneOp = new IP(noneParts.toString(), IPFormat.BINARY);

		return noneOp;
	}

	/**
	 * Retorna la direccion IP como resultado de la SUMA entre la de esta clase
	 * y la pasada como parametro
	 * 
	 * @param anotherIP
	 * @return sumIP
	 * @throws Exception
	 */
	public IP addOperation(IP anotherIP) throws Exception {

		String[] binIP = this.getBinIP().split("\\.");
		String[] bin2IP = anotherIP.getBinIP().split("\\.");

		String addParts = "";

		int plusSum = 0;

		// Recorro cada parte, desde la ultima a la primera
		for (int i = 3; i >= 0; i--) {

			// Convierto a decimal
			int decPart1 = Integer.parseInt(binIP[i], 2);
			int decPart2 = Integer.parseInt(bin2IP[i], 2);
			int decSum = decPart1 + decPart2 + plusSum;

			// Si se pasa de 255, dejo 255 en esta parte y le agrego lo que
			// sobra a la otra parte
			if (decSum > 255) {
				plusSum = decSum - 255;
				decSum = 0;
			} else
				plusSum = 0;

			// Convierto a binario
			String binSum = Integer.toBinaryString(decSum);

			// Si la parte tiene menos de 8 digitos, agregar 0's
			if (binSum.length() < 8) {
				StringBuffer zeros = new StringBuffer();
				for (int k = 0; k < 8 - binSum.length(); k++)
					zeros.append("0");
				binSum = zeros.toString() + binSum;
			}

			addParts = binSum + addParts;

			if (i > 0)
				addParts = "." + addParts;

		}

		IP addOp = new IP(addParts, IPFormat.BINARY);

		return addOp;
	}

	/**
	 * Retorna la direccion IP como resultado de la RESTA entre la de esta clase
	 * y la pasada como parametro
	 * 
	 * @param anotherIP
	 * @return restaIP
	 * @throws Exception
	 */
	public IP restaOperation(IP anotherIP) throws Exception {

		String[] binIP = this.getBinIP().split("\\.");
		String[] bin2IP = anotherIP.getBinIP().split("\\.");

		String restaParts = "";

		// Recorro cada parte, desde la ultima a la primera
		for (int i = 3; i >= 0; i--) {

			// Convierto a decimal
			int decPart1 = Integer.parseInt(binIP[i], 2);
			int decPart2 = Integer.parseInt(bin2IP[i], 2);
			int decResta = decPart1 - decPart2;

			// Convierto a binario
			String binResta = Integer.toBinaryString(decResta);

			// Si la parte tiene menos de 8 digitos, agregar 0's
			if (binResta.length() < 8) {
				StringBuffer zeros = new StringBuffer();
				for (int k = 0; k < 8 - binResta.length(); k++)
					zeros.append("0");
				binResta = zeros.toString() + binResta;
			}

			restaParts = binResta + restaParts;

			if (i > 0)
				restaParts = "." + restaParts;

		}

		IP restaOp = new IP(restaParts, IPFormat.BINARY);

		return restaOp;
	}

	/**
	 * Retorna la cantidad de 0's que tiene la direccion IP
	 * 
	 * @return zeroCount
	 */
	public int getZeroCount() {
		int zeroCount = 0;

		// Cuento la cantidad de 0's de la mascara
		for (int i = 0; i < this.binIP.length(); i++) {
			if (this.binIP.charAt(i) == '0')
				zeroCount++;
		}

		return zeroCount;

	}

	/**
	 * Sobreescribo el metodo equals()
	 * 
	 * @param anotherIP
	 * @return true/false
	 */
	public boolean equals(IP anotherIP) {
		return this.binIP.equals(anotherIP.getBinIP());
	}

	/**
	 * Sobreescribo el metodo toString() para que imprima la direccion IP
	 * representada en decimal, binario y hexa
	 */
	public String toString() {
		return this.getDecIP() + "  |  " + this.getBinIP() + "  |  "
				+ this.getHexaIP();
	}

	/**
	 * Compara la direccion IP local con la pasada como parametro Si la IP local
	 * es mayor, retorna 1 Si la IP local es menor, retorna -1 Si la IP local es
	 * igual, retorna 0
	 * 
	 * @param objIP
	 * @return compResult
	 */
	public int compareTo(Object objIP) {

		// Convierto la direccion IP (dec) a un arreglo
		String[] thisIP = this.getDecIP().split("\\.");
		String[] otherIP = ((IP) objIP).getDecIP().split("\\.");

		int comp = 0; // Indica el resultado de la comparacion (0: igual, 1:
		// mayor, -1: menor)
		int i = 0; // Cada una de las cuatro partes de la direccion IP (de 0 a
		// 3)

		// Ciclo mientras las partes sean iguales y pueda leer una siguiente
		// parte
		while ((comp == 0) && (i < 4)) {
			if (Integer.parseInt(thisIP[i]) > Integer.parseInt(otherIP[i])) {
				comp = 1;
			} else if (Integer.parseInt(thisIP[i]) < Integer
					.parseInt(otherIP[i])) {
				comp = -1;
			}
			i++;
		}

		return comp;
	}
}
